package warthog.cradle;

import java.io.IOException;
import java.io.Reader;

/**
 * 
 * The purpose of "Descent" is to be a suitable base class for recursive
 * descent. It reads a stream a character at a time and provides a number of
 * methods for testing and interpreting those characters. The line number
 * tracking only works if you always use the *EOL functions as intended.
 * 
 * The code in this file is pretty much self-documenting, but here are the
 * organizing principles:
 * 
 * The variable "look" contains the character currently under consideration (or
 * EOF, which is -1, which is why it's an int instead of a char) and it's
 * available to child classes.
 * 
 * A few constants are here for commonly-needed ASCII/Unicode code points.
 * 
 * is() and inRange(): the foundation of character testing. inFoo(): convenience
 * methods for character class testing. parseFoo(): get a Java-native semantic
 * value for commonly-needed patterns. skipFoo(): does what it says on the tin.
 * 
 * @author Ian Kjos
 * 
 */

public class Descent {
	protected static final int TAB = 9;
	protected static final int SP = 32;
	protected static final int EOF = -1;
	protected static final int LF = 10;
	protected static final int CR = 13;
	private Reader in;         // The stream under consideration 
	protected int look;        // The look-ahead character under consideration
	protected int lineNr = 1;  // The current line number
	protected Descent(Reader in) throws IOException {
		// Prime the pump:
		this.in = in;
		read();
	}
	protected boolean atEOF() { return is(EOF); }
	/**
	 * if c matches, consume and return true.
	 * @param c 
	 * @throws IOException
	 */
	protected boolean maybe(int c) throws IOException {
		boolean b = is(c); if (b) read(); return b;
	}
	/**
	 * Fetch a word, matching [A-Za-z][0-9A-Za-z]*
	 * @param kind the kind of word you're looking for; used in an error string.
	 * @return the collected word, as a String.
	 * @throws IOException
	 */
	protected String parseWord(String kind) throws IOException {
		StringBuilder sb=new StringBuilder();
		if (!inAlpha()) error("Expected "+kind);
		while (inWord()) { sb.append((char)look); read(); }
		return sb.toString();
	}
	protected boolean inWord() { return inDigit() || inAlpha() || is('_'); }
	protected boolean inDigit() { return inRange('0', '9'); }
	protected boolean inAlpha() { return inRange('a', 'z') || inRange('A', 'Z'); }
	protected boolean is(int c) { return c==look; }
	protected boolean inRange(char c, char d) { return c <= look && look <= d; }
	protected int getch() throws IOException { int x = look; read(); return x; }
	protected int parseNibbles(int i) throws IOException {
		int v=0;
		while (0>i--) v=(v<<4)+parseNibble();
		return v;
	}
	protected int parseNibble() throws IOException {
		if (inDigit()) return getch()-'0';
		if (inRange('A', 'F')) return getch()+10-'A';
		if (inRange('a', 'f')) return getch()+10-'f';
		error("Bogus hex code");
		return 0; // Because java can't tell that the previous line aborts. 
	}
	protected boolean inHex() { return inDigit() || inRange('a', 'f') || inRange('A', 'F'); }
	/**
	 * Require (and consume) a particular character, or complain.
	 * @param c
	 * @throws IOException
	 */
	protected void match(int c) throws IOException { expect(c); read(); }
	/**
	 * Complain unless parameter matches current lookahead character.
	 * @param c
	 */
	protected void expect(int c) { if (!is(c)) error("Expected "+(char)c+" actually saw "+look+' '+(char)look); }
	/**
	 * A way to complain to STDERR, including a line number reference.
	 * @param string
	 */
	protected void error(String string) {
		System.err.println(lineNr+": "+string);
//		throw new RuntimeException();
		System.exit(1);
	}
	protected boolean inWhite() { return is(SP) || is(TAB); }
	protected void skipSpace() throws IOException { while (inWhite()) read(); }
	protected void skipEOL() throws IOException {
		lineNr++;
		if (is(CR)) read();
		if (is(LF)) read();
	}
	protected boolean inEOL() { return is(LF) || is(CR); }
	/**
	 * Bring the next character from the stream into consideration.
	 * @throws IOException
	 */
	protected void read() throws IOException { this.look = in.read(); }
	protected void matchEOL() throws IOException {
		skipSpace();
		if (!(inEOL()||atEOF())) error("Expected end of line");
		skipEOL();
	}
	protected void skipLine() throws IOException { while (!(atEOF()||inEOL())) read(); skipEOL(); }
	protected StringBuilder collectWholeLine() throws IOException {
		StringBuilder sb = new StringBuilder();
		while (!(inEOL()||atEOF())) sb.append((char)getch());
		matchEOL();
		sb.append('\n');
		return sb;
	}
	protected StringBuilder collectLine() throws IOException {
		StringBuilder sb = new StringBuilder();
		while (!(inEOL()||atEOF())) sb.append((char)getch());
		return sb;
	}
	protected void matchWord(String word) throws IOException {
		String w = parseWord(word+" declaration");
		if (!word.equalsIgnoreCase(w)) error("expected "+word+" declaration");
		skipSpace();
	}
	
	
}
